-- Copyright (c) 2024 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local json = require 'cjson'
local ipmi = require 'ipmi'
local log = require 'mc.logging'
local custom_messages = require 'messages.custom'
local ncsi_info = require 'ncsi.ncsi_info'
local ncsi_comm = require 'ncsi.ncsi_comm'
local ncsi_worker = require 'ncsi.ncsi_worker'
local ncsi_set_worker = require 'ncsi.ncsi_set_worker'
local ipmi_struct = require 'network_adapter.ipmi.ipmi'
local client = require 'network_adapter.client'

local cc = ipmi.types.Cc

local MANUFACTURE_ID<const> = 0x0007db

local ncsi_service = {}
ncsi_service.__index = ncsi_service

function ncsi_service.new(db, bus)
    local ncsi_comm_obj = ncsi_comm.new(db, bus)
    return setmetatable({db = db, bus = bus,
        ncsi_info = ncsi_info.new(db, bus),
        work = ncsi_worker.new(),
        set_work = ncsi_set_worker.new(),
        ncsi_comm = ncsi_comm_obj}, ncsi_service)
end

function ncsi_service:init()
end

local IPMI_LOM_NCSI_TYPE<const> = 0
local IPMI_PCIE_NCSI_TYPE<const> = 1
local IPMI_MANAGEMENT_TYPE<const> = 2
local IPMI_LOM2_NCSI_TYPE<const> = 3
local IPMI_OCP_NCSI_TYPE<const> = 4
local IPMI_OCP2_NCSI_TYPE<const> = 5
local IPMI_CARD_MAX_TYPE<const> = 6

local INVALID_VALUE<const> = 0xff

local ENABLED<const> = 1
local DISABLED<const> = 0

local NCSI_RX_CMD_HEAD_LEN<const> = 2
local NCSI_RX_2PORTS_DATA_LEN<const> = 4
local NCSI_RX_4PORTS_DATA_LEN<const> = 6

local NETWORK_MSG_SET_NCSI_RX_CHANNEL_STATE<const> = 13

local IPMI_CARD_TYPE_STR<const> = {
    [IPMI_LOM_NCSI_TYPE] = 'Lom',
    [IPMI_PCIE_NCSI_TYPE] = 'ExternalPCIe',
    [IPMI_MANAGEMENT_TYPE] = 'Dedicated',
    [IPMI_LOM2_NCSI_TYPE] = 'Lom2',
    [IPMI_OCP_NCSI_TYPE] = 'Ocp',
    [IPMI_OCP2_NCSI_TYPE] = 'Ocp2'
}

local IPMI_CARD_TYPE_STR_TO_NUM<const> = {
    Lom = IPMI_LOM_NCSI_TYPE,
    ExternalPCIe = IPMI_PCIE_NCSI_TYPE,
    Dedicated = IPMI_MANAGEMENT_TYPE,
    Lom2 = IPMI_LOM2_NCSI_TYPE,
    Ocp = IPMI_OCP_NCSI_TYPE,
    Ocp2 = IPMI_OCP2_NCSI_TYPE
}

local function check_set_ncsi_rx_channel_data(ncsi_rx_channel_state, ipmi_card_type, port1_num, port2_num)
    if ncsi_rx_channel_state ~= DISABLED and ncsi_rx_channel_state ~= ENABLED then
        return false
    end

    if ipmi_card_type == IPMI_MANAGEMENT_TYPE then
        return false
    end

    if port1_num == port2_num and port1_num ~= INVALID_VALUE then
        return false
    end

    if ncsi_rx_channel_state == ENABLED and ipmi_card_type >= IPMI_CARD_MAX_TYPE then
        return false
    end

    return true
end

local function check_ncsi_port_exists(db, ipmi_card_type, port_num)
    -- MgmtPortId非0表示端口存在
    local obj = db:select(db.NcsiNCPortInfo):where(db.NcsiNCPortInfo.Type:eq(IPMI_CARD_TYPE_STR[ipmi_card_type]),
        db.NcsiNCPortInfo.PortId:eq(port_num), db.NcsiNCPortInfo.MgmtPortId:ne(0)):first()
    return obj ~= nil
end

local function check_valid_port_for_set_ncsi_rx(db, data, ipmi_card_type, data_len)
    local port_index
    local port_cnt = data_len - NCSI_RX_CMD_HEAD_LEN
    for i = 1, port_cnt do
        port_index = NCSI_RX_CMD_HEAD_LEN + i
        local port_num = data:byte(port_index)
        -- ipmi命令请求的端口号需要加一对应属性序号
        if port_num == INVALID_VALUE or (not check_ncsi_port_exists(db, ipmi_card_type, port_num + 1)) then
            return false
        end
    end

    return true
end

local function set_ncsi_rx_channel_state_prop(ncsi_rx_channel_state, ipmi_card_type, ipmi_ports, port_obj)
    if ncsi_rx_channel_state == DISABLED then
        port_obj.RxEnable = false
        return
    end

    local type_str = IPMI_CARD_TYPE_STR[ipmi_card_type]
    for _, port_num in pairs(ipmi_ports) do
        -- ipmi命令请求的端口号需要加一对应属性序号
        if port_obj.Type == type_str and port_obj.PortId == port_num + 1 then
            port_obj.RxEnable = true
            return
        end
    end
    port_obj.RxEnable = false
end

function ncsi_service:set_nsci_rx_msg(ncsi_rx_channel_state, ipmi_card_type, ipmi_ports)
    -- 获取当前使用的端口id
    local used_id
    client:ForeachEthernetInterfacesObjects(function(obj)
        used_id = obj.PortId
    end)

    -- 根据端口id获取当前使用的网口信息
    local used_obj = used_id and
        self.db:select(self.db.NcsiNCPortInfo):where(self.db.NcsiNCPortInfo.MgmtPortId:eq(used_id)):first()
    local used_port = used_obj and {type = used_obj.Type, port_num = used_obj.PortId} or {}

    -- 遍历收集所有可用的网口，并且设置网口的RX使能属性
    local db_ports = {}
    self.db:select(self.db.NcsiNCPortInfo):fold(function(port_obj)
        db_ports[port_obj.PortId] = {
            type = port_obj.Type,
            port_num = port_obj.PortId,
            package_id = port_obj.PackageId,
            channel_id = port_obj.ChannelId
        }

        -- 当前使用的网口不禁用
        if ncsi_rx_channel_state == DISABLED and used_port and port_obj.Type == used_port.type and
            port_obj.PortId == used_port.port_num then
            return
        end

        set_ncsi_rx_channel_state_prop(ncsi_rx_channel_state, ipmi_card_type, ipmi_ports, port_obj)
    end)

    local network_msg = {
        cmd = NETWORK_MSG_SET_NCSI_RX_CHANNEL_STATE,
        ncsi_rx_channel_state = ncsi_rx_channel_state,
        ipmi_card_type = ipmi_card_type,
        ipmi_ports = ipmi_ports,
        db_ports = db_ports,
        used_port = used_port
    }
    return self.set_work:send(json.encode(network_msg))
end

function ncsi_service:set_ncsi_rx_channel_from_ipmi(req, ctx)
    local data_len = req.Length
    if data_len ~= NCSI_RX_2PORTS_DATA_LEN and data_len ~= NCSI_RX_4PORTS_DATA_LEN then
        ipmi.ipmi_operation_log(ctx, 'NetworkAdapter', 'Set NC-SI RX channels failed');
        error(custom_messages.IPMIInvalidFieldRequest())
    end

    if req.Flag ~= 0 or req.Offset ~= 0 then
        ipmi.ipmi_operation_log(ctx, 'NetworkAdapter', 'Set NC-SI RX channels failed');
        error(custom_messages.IPMIInvalidFieldRequest())
    end

    local rsp = ipmi_struct.SetIPMCConfiguration.rsp
    local ncsi_rx_channel_state, ipmi_card_type, port1_num, port2_num = req.Data:byte(1, 4)
    if not check_set_ncsi_rx_channel_data(ncsi_rx_channel_state, ipmi_card_type, port1_num, port2_num) then
        ipmi.ipmi_operation_log(ctx, 'NetworkAdapter', 'Set NC-SI RX channels failed');
        rsp.CompletionCode = cc.InvalidFieldRequest
        rsp.ManufactureId = MANUFACTURE_ID
        return rsp
    end

    if ncsi_rx_channel_state == ENABLED and
        (not check_valid_port_for_set_ncsi_rx(self.db, req.Data, ipmi_card_type, data_len)) then
        ipmi.ipmi_operation_log(ctx, 'NetworkAdapter', 'Set NC-SI RX channels failed');
        rsp.CompletionCode = cc.InvalidFieldRequest
        rsp.ManufactureId = MANUFACTURE_ID
        return rsp
    end

    local port3_num = req.Data:byte(5) or INVALID_VALUE
    local port4_num = req.Data:byte(6) or INVALID_VALUE
    if not self:set_nsci_rx_msg(ncsi_rx_channel_state, ipmi_card_type,
        {port1_num, port2_num, port3_num, port4_num}) then
        ipmi.ipmi_operation_log(ctx, 'NetworkAdapter', 'Set NC-SI RX channels failed');
        error(custom_messages.IPMICommandResponseCannotProvide())
    end

    if ncsi_rx_channel_state == ENABLED then
        if data_len == NCSI_RX_4PORTS_DATA_LEN then
            ipmi.ipmi_operation_log(ctx, 'NetworkAdapter',
                'Enabled NC-SI RX channels for %s ports %d and %d and %d and %d successfully',
                IPMI_CARD_TYPE_STR[ipmi_card_type] or '', port1_num + 1, port2_num + 1, port3_num + 1, port4_num + 1)
        else
            ipmi.ipmi_operation_log(ctx, 'NetworkAdapter',
                'Enabled NC-SI RX channels for %s ports %d and %d successfully',
                IPMI_CARD_TYPE_STR[ipmi_card_type] or '', port1_num + 1, port2_num + 1)
        end
    else
        ipmi.ipmi_operation_log(ctx, 'NetworkAdapter', 'Disabled NC-SI RX channels for all inactive ports successfully')
    end
    rsp.CompletionCode = cc.Success
    rsp.ManufactureId = MANUFACTURE_ID
    return rsp
end

local function get_ncsi_channel_state(db, rsp, data_len)
    local ncsi_rx_channel_state = DISABLED
    local ipmi_card_type = INVALID_VALUE
    local ports = {}
    db:select(db.NcsiNCPortInfo):fold(function(port_obj)
        if not port_obj.RxEnable or #ports >= data_len - NCSI_RX_CMD_HEAD_LEN then
            return
        end

        ncsi_rx_channel_state = ENABLED
        ipmi_card_type = IPMI_CARD_TYPE_STR_TO_NUM[port_obj.Type]
        table.insert(ports, port_obj.PortId - 1)
    end)

    while #ports < data_len - NCSI_RX_CMD_HEAD_LEN do
        table.insert(ports, INVALID_VALUE)
    end

    rsp.Data = string.char(ncsi_rx_channel_state, ipmi_card_type) .. string.char(table.unpack(ports))
end

function ncsi_service:get_ncsi_rx_channel_from_ipmi(req, ctx)
    local data_len = req.Length
    if req.Offset ~= 0 or data_len ~= NCSI_RX_2PORTS_DATA_LEN and data_len ~= NCSI_RX_4PORTS_DATA_LEN then
        log:debug('Invalid parameter,offset=%d,length=%d', req.Offset, data_len)
        error(custom_messages.IPMIInvalidFieldRequest())
    end

    local rsp = ipmi_struct.GetIPMCConfiguration.rsp
    get_ncsi_channel_state(self.db, rsp, data_len)
    rsp.CompletionCode = cc.Success
    rsp.ManufactureId = MANUFACTURE_ID
    return rsp
end

return ncsi_service
